<?php
/**
 * Created by PhpStorm.
 * User: 赵修宇
 * Date: 2018/5/20
 * Time: 10:22
 */

namespace Home\Model;

use Think\Model;
use Think\Db;

class CosSimilarityModel extends Model
{
    /**
     * 构造
     * CosSimilarityController constructor.
     */
    public function __construct()
    {
        $this->table = 'cosine';
        $this->recommended = [];
        $this->people = [];
    }

    public function pri($data)
    {
        echo '<pre>';
        print_r($data);
        echo '</pre>';
    }

    /**
     * 接口入口
     */
    public function cosine()
    {
        try {
            $pointPeople = $this->peopleCosine();  //return $this->people
            $pointProduct = $this->productCosine();
            $recommend = new RecommendModel();
            $recommend->recommend($pointPeople, $pointProduct);
        } catch (CosException $e) {
            return $e->getMessage();
        }
    }

    /**
     * 计算与Tom喜好相似的人
     */
    public function peopleCosine()
    {
        $tom = $this->getTom();
        $hundredPoint = $this->abstractPoint();
        $point = $this->calculate($tom, $hundredPoint);
        return array_column($point, 'user_id');
        $this->recommend($tom, $point, $hundredPoint);    //已排序
    }

    /**
     * 模拟Tom数据
     * @return array
     */
    public function getTom()
    {
        $json = '{"user_id":2333,"content":{"2333":[{"pid":1,"grade":2},{"pid":2,"grade":4},{"pid":3,"grade":3},{"pid":4,"grade":4},{"pid":5,"grade":3},{"pid":6,"grade":4},{"pid":7,"grade":4},{"pid":8,"grade":0},{"pid":9,"grade":4},{"pid":10,"grade":3},{"pid":11,"grade":2},{"pid":12,"grade":2},{"pid":13,"grade":5},{"pid":14,"grade":3},{"pid":15,"grade":0},{"pid":16,"grade":5},{"pid":17,"grade":1},{"pid":18,"grade":4},{"pid":19,"grade":5},{"pid":20,"grade":2},{"pid":21,"grade":3},{"pid":22,"grade":4},{"pid":23,"grade":2},{"pid":24,"grade":2},{"pid":25,"grade":3},{"pid":26,"grade":4},{"pid":27,"grade":0},{"pid":28,"grade":3},{"pid":29,"grade":0},{"pid":30,"grade":0},{"pid":31,"grade":3},{"pid":32,"grade":3},{"pid":33,"grade":5},{"pid":34,"grade":0},{"pid":35,"grade":2},{"pid":36,"grade":2},{"pid":37,"grade":5},{"pid":38,"grade":0},{"pid":39,"grade":2},{"pid":40,"grade":3},{"pid":41,"grade":3},{"pid":42,"grade":5},{"pid":43,"grade":5},{"pid":44,"grade":3},{"pid":45,"grade":2},{"pid":46,"grade":0},{"pid":47,"grade":2},{"pid":48,"grade":3},{"pid":49,"grade":5},{"pid":50,"grade":1}]}}';
        $res = json_decode($json, true);
        $grade = array_column($res['content']['2333'], 'grade');
        return $grade;
    }

    /**
     * 抽象点集
     * @return array
     */
    public function abstractPoint()
    {
        $hundred = $this->getHundred();
//        $hundred = array_map(function ($v) {
//            return $v = get_object_vars($v);
//        }, $hundred);
        $content = array_column($hundred, 'content', 'user_id');
        $content = array_map(function (&$v) {
            return $v = json_decode($v, true);
        }, $content);
        $point = [];
        foreach ($content as $cv) {
            foreach ($cv as $k => $v) {
                $point[$k] = array_column($v, 'grade');
            }
        }
        return $point;
    }

    /**
     * 获取其余所有用户数据
     * @return mixed
     */
    public function getHundred()
    {
        $hundred = $this->sel($this->table, array('user_id', '!=', 2333));    //索引数组 , 值为对象
        return $hundred;
    }

    public function sel($table, $where)
    {
        $db = M($table);
        $res = $db->where($where[0], $where[1], $where[2])->select();
//        $res = DB::table($table)->where($where[0], $where[1], $where[2])->get();
        return $res;
    }

    /**
     * 递归计算两向量夹角余弦
     * @param $unit
     * @param $set
     * @return mixed
     */
    public function calculate($unit, $set, $idName = 'user_id')
    {
//$this->pri($unit);
//$this->pri($set);die;
        foreach ($set as $k => $v) {
            $angle[$k] = $this->formula($unit, $v); //夹角余弦 float , 失败返回false
        }
        //降序排序
        $angleKey = array_keys($angle);
        $angle = $this->arrMerge($angle, $angleKey, $idName);
        array_multisort($angle, SORT_DESC);
        return $angle;
    }

    /**
     * cosθ 数学公式
     * @param $standard
     * @param $element
     * @return bool|float|int
     */
    public function formula($standard, $element)
    {
        if (count($standard) != count($element)) {
            return false;
        }
        $productSigma = 0;   //乘积累加器
        $powerSigmaA = 0;     //乘方累加器A
        $powerSigmaB = 0;     //乘方累加器B
        $denominator = 0;    //分母

        for ($i = 0; $i < count($element); $i++) {
            $productSigma += $this->multiplication($standard[$i], $element[$i]);
            $powerSigmaA += $this->multiplication($standard[$i], $standard[$i]);
            $powerSigmaB += $this->multiplication($element[$i], $element[$i]);
        }
        $denominator = $this->multiplication(sqrt($powerSigmaA), sqrt($powerSigmaB));
        return $productSigma / $denominator;
    }

    /**
     * 乘法
     * @param $multiplicand
     * @param $multiplier
     * @return float
     */
    public function multiplication($multiplicand, $multiplier)
    {
        return floatval($multiplicand) * floatval($multiplier);
    }

    /**
     * 合并数组
     * @param $arr1
     * @param $arr2
     */
    public function arrMerge(&$arr1, $arr2, $idName)
    {
        $res = [];
        foreach ($arr1 as $k => $v) {
//            unset($arr1[$k]);
            $res[$k]['cosine'] = $v;
            $res[$k][$idName] = $arr2[$k - 1];
        }
        return $res;
    }

    /**
     * 按相似度降序取Tom未评论 , 但相似人评论且评分>=3的 , 取到五件商品为止
     * @param $standard
     * @param $point
     * @param $hundredPoint
     */
    public function recommend($standard, $point, $hundredPoint)
    {
        array_walk($point, function ($v) use ($standard, $hundredPoint) {
            $this->getUnascertained($standard, $hundredPoint[$v['user_id']]);   //return $this->people
        });
        //去重取前五
        $this->people = array_slice(array_unique($this->people), 0, 5);
    }

    /**
     * 获取未评价
     * @param $standard
     * @param $userEvaluation
     */
    public function getUnascertained($standard, $userEvaluation)
    {
        array_walk($standard, function ($v, $k) use ($userEvaluation) {
            if ($v == 0 && $userEvaluation[$k] >= 3) {
                return $this->people[] = $k;
            }
        });
    }

    /**
     * 计算Tom最喜欢的商品最相似的商品
     */
    public function productCosine()
    {
        $tom = $this->getTom();
        $favorite = array_keys($tom, 5);
        //获取最喜欢的商品点集
        $proPoint = $this->getProPoint($favorite);
        //获取除待评价用户5星评价商品外其他所有商品点集
        $otherPoint = $this->getOther($favorite);
//$this->pri($proPoint);
//$this->pri($otherPoint);die;
        //获取相似商品
        $res = $this->getSimilarPro($proPoint, $otherPoint);
        return $res;
    }

    public function getProPoint($favorite)
    {
        //若最喜欢大于5 , 只取前5 TODO:目前取的是第一个
        if (count($favorite) > 5) {
            $favorite = array_slice($favorite,0,1);
        }
        $proPoint = [];
        array_map(function ($v) use (&$proPoint) {
            //获取商品详细信息
            $proPoint[$v] = $this->getProduct($v);
            unset($proPoint[$v]['id'], $proPoint[$v]['pid'], $proPoint[$v]['created_at'], $proPoint[$v]['updated_at']);
            $proPoint[$v] = array_values($proPoint[$v]);
        }, $favorite);
        return $proPoint;
    }

    public function getSimilarPro($proPoint, $otherPoint)
    {
        //记录待评价用户被选取的最喜欢商品ID
        $favorite_pro_id = array_keys($proPoint);
        $proPoint = $proPoint[$favorite_pro_id[0]]; //去掉ID健名
        $res = $this->calculate($proPoint, $otherPoint, 'productId');
        $res = array_column($res, 'productId');
        foreach ($res as $k => $v){
            //不规则索引数组
            if (empty($v)){
                unset($res[$k]);
            }
        }
        return $res;
    }

    public function getProduct($pid)
    {
//        $res = $this->sel('recommendProduct', array('pid', '=', $pid));
        $res = M('recommendProduct')->where("pid=$pid")->select();
        if (empty($res)) {
//            throw new CosException(['status' => 1, 'errmsg' => 'no such product']);
//            $this->error(['status' => 1, 'errmsg' => 'no such product']);
        }
//        return get_object_vars($res[0]);
        return $res[0];
    }

    public function getOther($pidArr)
    {
        $sql = M('recommendProduct');
//        $sql = DB::table('product');
        $where = [];
        array_walk($pidArr, function ($v) use (&$where) {
//            $sql->where('pid', '!=', $v);
            $where[] = "pid!=$v";
        });
//        $sql = $sql->get();
        $sql = $sql->where($where)->select();
        $res = [];
        //格式化点集数组
        array_walk($sql, function (&$v) use (&$res) {
//            $vArr = get_object_vars($v);
            $vArr = $v;
            $res[$vArr['pid']] = $vArr;
            unset($res[$vArr['pid']]['id'], $res[$vArr['pid']]['pid'], $res[$vArr['pid']]['created_at'], $res[$vArr['pid']]['updated_at']);
            $res[$vArr['pid']] = array_values($res[$vArr['pid']]);
        });
        return $res;
    }
}